#include <errno.h>
#include <limits.h>
#include <stdarg.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>

#include <SDL.h>
#include <SDL_audio.h>
#include <SDL_events.h>
#include <SDL_mutex.h>

#include "au.h"
#include "adev.h"
#include "common.h"

#ifndef SDL_BSIZE
#define SDL_BSIZE 0x4000
#endif

enum {
	A_NULL,
	A_ENC,
	A_RATE,
	A_CHAN
};

struct adev {
	uint8_t			 buffer[SDL_BSIZE];
	SDL_AudioSpec		 spec;
	SDL_AudioDeviceID	 dev;
	SDL_mutex		*mtx;
	SDL_cond		*cond;
	SDL_cond		*done;
	const char		*device;
	size_t			 bcount;
	size_t			 waiting;
	Uint32			 etype;
	uint32_t		 enc;
	int			 mode;
	int			 needopen;
	int			 isdone;
};

static int	correct_audio(SDL_AudioSpec *, void *, size_t);
static void	sdl_callback(void *, uint8_t *, int);

const struct a_lookup stab[] = {
	{ "enc", A_ENC },
	{ "encoding", A_ENC },
	{ "rate", A_RATE },
	{ "chan", A_CHAN },
	{ "channel", A_CHAN },
	{ "channels", A_CHAN },
	{ NULL, A_NULL }
};

struct adev *
ad_open(const char *dev, int mode)
{
	struct adev *a;

	if (SDL_Init(SDL_INIT_AUDIO) != 0)
		return (NULL);
	if ((a = malloc(sizeof(struct adev))) == NULL) {
		SDL_Quit();
		return (NULL);
	}
	a->dev = 0;
	a->mtx = NULL;
	a->cond = NULL;
	a->done = NULL;
	memset(&a->spec, 0, sizeof(SDL_AudioSpec));
	a->spec.size = sizeof(a->buffer);
	a->spec.callback = &sdl_callback;
	a->spec.userdata = a;
	a->bcount = 0;
	a->waiting = 0;
	a->needopen = 1;
	a->isdone = 0;
	a->dev = 0;
	a->device = dev;
	a->enc = 0;
	a->mode = mode == A_READ;
	if ((a->etype = SDL_RegisterEvents(1)) == (Uint32)-1)
		goto err;
	if ((a->mtx = SDL_CreateMutex()) == NULL ||
	    (a->cond = SDL_CreateCond()) == NULL ||
	    (a->done = SDL_CreateCond()) == NULL)
		goto err;
	return (a);
err:
	ad_close(a);
	return (NULL);
}

struct adev *
ad_fpopen(FILE *fp, int mode)
{

	ERR(EOPNOTSUPP, NULL);
}

void
ad_close(struct adev *a)
{

	if (a == NULL)
		return;
	a->isdone = 1;
	if (a->mtx != NULL)
		(void)SDL_LockMutex(a->mtx);
	while (a->waiting > 0) {
		(void)SDL_CondSignal(a->cond);
		(void)SDL_CondWait(a->done, a->mtx);
	}
	if (a->mtx != NULL)
		SDL_UnlockMutex(a->mtx);
	if (a->dev > 0)
		SDL_CloseAudioDevice(a->dev);
	if (a->mtx != NULL)
		SDL_DestroyMutex(a->mtx);
	if (a->cond != NULL)
		SDL_DestroyCond(a->cond);
	SDL_Quit();
	free(a);
}

int
ad_limit(struct adev *a)
{

	ERR(ENOSYS, -1);
}

int
ad_get(struct adev *a, const char *s, ...)
{
	va_list ap;
	ssize_t r;
	int n;
	int v;

	if (a == NULL || s == NULL)
		ERR(EINVAL, -1);
	(void)SDL_LockMutex(a->mtx);
	va_start(ap, s);
	for (n = 0; (r = a_lookup(stab, s, &v)) > 0; n++, s += r)
		switch (v) {
		case A_ENC:
			*va_arg(ap, uint32_t *) = a->enc;
			break;
		case A_RATE:
			*va_arg(ap, uint32_t *) = a->spec.freq;
			break;
		case A_CHAN:
			*va_arg(ap, uint32_t *) = a->spec.channels;
			break;
		default:
			goto err;
		}
	if (r == 0) {
		(void)SDL_UnlockMutex(a->mtx);
		va_end(ap);
		return (n);
	}
err:
	(void)SDL_UnlockMutex(a->mtx);
	va_end(ap);
	ERR(EINVAL, n);
}

int
ad_set(struct adev *a, const char *s, ...)
{
	va_list ap;
	ssize_t r;
	int n;
	int v;

	if (a == NULL || s == NULL)
		ERR(EINVAL, -1);
	(void)SDL_LockMutex(a->mtx);
	if (a->dev > 0) {
		SDL_CloseAudioDevice(a->dev);
		a->dev = 0;
	}
	a->needopen = 1;
	va_start(ap, s);
	for (n = 0; (r = a_lookup(stab, s, &v)) > 0; n++, s += r)
		switch (v) {
		case A_ENC:
			switch ((a->enc = va_arg(ap, uint32_t))) {
			case AU_ULAW:
				a->enc = AU_PCM16;
				a->spec.format = AUDIO_S16MSB;
				goto err;
			case AU_PCM8:
				a->spec.format = AUDIO_S8;
				break;
			case AU_PCM16:
				a->spec.format = AUDIO_S16MSB;
				break;
			case AU_PCM24:
				a->enc = AU_PCM32;
				a->spec.format = AUDIO_S32MSB;
				goto err;
			case AU_PCM32:
				a->spec.format = AUDIO_S32MSB;
				break;
			case AU_FLOAT32:
				a->spec.format = AUDIO_F32MSB;
				break;
			case AU_FLOAT64:
				a->enc = AU_FLOAT32;
				a->spec.format = AUDIO_F32MSB;
				goto err;
			case AU_ALAW:
				a->enc = AU_PCM16;
				a->spec.format = AUDIO_S16MSB;
				goto err;
			default:
				goto err;
			}
			break;
		case A_RATE:
			a->spec.freq = va_arg(ap, uint32_t);
			break;
		case A_CHAN:
			a->spec.channels = va_arg(ap, uint32_t);
			break;
		default:
			goto err;
		}
	if (r == 0) {
		(void)SDL_UnlockMutex(a->mtx);
		va_end(ap);
		return (n);
	}
err:
	(void)SDL_UnlockMutex(a->mtx);
	va_end(ap);
	ERR(EINVAL, n);
}

ssize_t
ad_read(struct adev *a, void *b, size_t s)
{
	SDL_Event e;
	size_t i;
	size_t n;

	if (a == NULL || b == NULL || !a->mode)
		ERR(EINVAL, -1);
	if (s > UINT32_MAX)
		ERR(ERANGE, -1);
	if (s == 0)
		return (0);
	(void)SDL_LockMutex(a->mtx);
	if (a->needopen) {
		if ((a->dev = SDL_OpenAudioDevice(a->device, a->mode,
		    &a->spec, NULL, 0)) < 2)
			goto err;
		SDL_PauseAudioDevice(a->dev, 0);
		a->needopen = 0;
	}
	for (i = 0; i < s && !a->isdone;) {
		/* Wait for the buffer to have data */
		while (a->bcount == 0 && !a->isdone) {
			if (SDL_CondSignal(a->cond) != 0)
				goto err;
			(void)SDL_UnlockMutex(a->mtx);
			if (SDL_WaitEvent(&e) != 1)
				goto err;
			(void)SDL_LockMutex(a->mtx);
			if (e.type == SDL_AUDIODEVICEREMOVED &&
			    e.adevice.which == a->dev)
				goto ret;
			if (e.type != a->etype)
				continue;
		}
		n = MIN(s - i, a->bcount);
		if (correct_audio(&a->spec, a->buffer, n) != 0)
			goto err;
		(void)memcpy((char *)b + i, a->buffer, n);
		if (a->bcount > n)
			(void)memmove(a->buffer, a->buffer + n,
				    a->bcount - n);
		a->bcount -= n;
		i += n;
	}
ret:
	(void)SDL_UnlockMutex(a->mtx);
	return (i);
err:
	(void)SDL_UnlockMutex(a->mtx);
	return (-1);
}

ssize_t
ad_write(struct adev *a, const void *b, size_t s)
{
	SDL_Event e;
	size_t i;
	size_t n;

	if (a == NULL || b == NULL || a->mode)
		ERR(EINVAL, -1);
	if (s > UINT32_MAX)
		ERR(ERANGE, -1);
	if (s == 0)
		return (0);
	(void)SDL_LockMutex(a->mtx);
	if (a->needopen) {
		if ((a->dev = SDL_OpenAudioDevice(a->device, a->mode,
		    &a->spec, NULL, 0)) < 2)
			goto err;
		SDL_PauseAudioDevice(a->dev, 0);
		a->needopen = 0;
	}
	for (i = 0; i < s && !a->isdone;) {
		/* Wait for the buffer to have free space */
		while (a->bcount >= sizeof(a->buffer) && !a->isdone) {
			if (SDL_CondSignal(a->cond) != 0)
				goto err;
			(void)SDL_UnlockMutex(a->mtx);
			if (SDL_WaitEvent(&e) != 1)
				goto err;
			(void)SDL_LockMutex(a->mtx);
			if (e.type == SDL_AUDIODEVICEREMOVED &&
			    e.adevice.which == a->dev)
				goto ret;
			if (e.type != a->etype)
				continue;
		}
		n = MIN(s - i, sizeof(a->buffer) - a->bcount);
		(void)memcpy(a->buffer + a->bcount, (char *)b + i, n);
		if (correct_audio(&a->spec,
		    a->buffer + a->bcount, n) != 0)
			goto err;
		i += n;
		a->bcount += n;
	}
ret:
	(void)SDL_UnlockMutex(a->mtx);
	return (i);
err:
	(void)SDL_UnlockMutex(a->mtx);
	return (-1);
}

void
sdl_callback(void *ctx, uint8_t *b, int s)
{
	struct adev *a = ctx;
	SDL_Event e;
	int i;
	int n;

	if (ctx == NULL || b == NULL || s <= 0)
		return;
	if (a->isdone)
		return;
	if (SDL_LockMutex(a->mtx) != 0) {
		a->isdone = 1;
		return;
	}
	e.type = a->etype;
	if (a->mode) { /* read */
		for (i = 0; i < s && !a->isdone;) {
			/* Wait for the buffer to have free space */
			a->waiting++;
			while (a->bcount >= sizeof(a->buffer) &&
			    !a->isdone)
				if (SDL_PushEvent(&e) != 1 ||
				    SDL_CondWait(a->cond, a->mtx) != 0)
					a->isdone = 1;
			a->waiting--;
			if (a->bcount < sizeof(a->buffer)) {
				n = MIN(sizeof(a->buffer) - a->bcount,
				    s - i);
				(void)memcpy(a->buffer + a->bcount,
				    b + i, n);
				i += n;
				a->bcount += n;
			}
		}
		if (a->bcount >= sizeof(a->buffer) && !a->isdone)
			(void)SDL_PushEvent(&e);
	} else { /* write */
		for (i = 0; i < s && !a->isdone;) {
			/* Wait for the buffer to have data */
			a->waiting++;
			while (a->bcount == 0 && !a->isdone)
				if (SDL_PushEvent(&e) != 1 ||
				    SDL_CondWait(a->cond, a->mtx) != 0)
					a->isdone = 1;
			a->waiting--;
			n = MIN(s - i, a->bcount);
			(void)memcpy(b + i, a->buffer, n);
			if (a->bcount > s - i)
				(void)memmove(a->buffer, a->buffer + n,
				    a->bcount - n);
			i += n;
			a->bcount -= n;
		}
		if (a->bcount == 0 && !a->isdone)
			(void)SDL_PushEvent(&e);
	}
	if (a->isdone) {
		(void)SDL_PushEvent(&e);
		(void)SDL_CondSignal(a->done);
	}
	(void)SDL_UnlockMutex(a->mtx);
}

int
correct_audio(SDL_AudioSpec *spec, void *b, size_t s)
{
	uint8_t *u;
	size_t i;
	unsigned bs;

	if (spec == NULL || b == NULL ||
	    (bs = SDL_AUDIO_BITSIZE(spec->format)) > 32)
		ERR(EINVAL, -1);
	if (bs == 0)
		return (0);
	if (SDL_AUDIO_ISLITTLEENDIAN(spec->format))
		au_encbswap(AU_PCM8 + (bs - 1) / 8, b, s);
	if (SDL_AUDIO_ISUNSIGNED(spec->format))
		for (u = b, i = 0, bs = (bs + 7) / 8; i < s; i += bs)
			u[i] ^= 0x80;
	return (0);
}
